Python デコレーター:メタプログラミングの表現方法
概要
この投稿は、Pythonのデコレーター、メタプログラミング、関数型プログラミングに関する個人的な研究の結果です。しかし、Bruce Eckel氏とオープンソースブック "Python 3 Patterns, Recipes and Idioms"の関係者には、このテーマに関する多くの貴重な情報を提供してくれたことに感謝したいと思います。彼らの仕事をチェックするには、記事の最後にあるリソースセクションを参照してください。 Pythonは関数型ですか?
まあ、違いますね。Pythonは強力なオブジェクト指向プログラミング言語であり、例えばScala(ちなみにこれはとても良い言語です)のようにOOPと関数型を混在させるようなことはしません。
しかし、Pythonは関数型プログラミングから取り入れた機能をいくつか提供しています。ジェネレータとイテレータはその1つで、純粋でない関数型プログラミング言語の中で、Pythonだけがそれらをツールボックスに備えているわけではありません。
おそらく、関数型言語の最も特徴的な機能は、関数がファーストクラスオブジェクトであることです。つまり、関数は他の関数の引数として渡したり、関数から返されたりすることができます。関数型言語では、関数は利用可能なデータ型の1つに過ぎません(非常に大雑把な単純化ではありますが)。
Pythonには、関数的な動作を可能にする3つの重要な機能があります:参照(Reference)、関数オブジェクト、呼び出し可能オブジェクト(Callable) です。
参照
Pythonの変数は共通の性質を持っています。これは、変数はそれ自体型付けされておらず、純粋なメモリアドレスであり、関数は引数の入力データ型を宣言しないことを意味します(段階的な型付けはさておき)。Pythonの多態性(Polymorphism) は 委譲(delegation)に基づいており、関数の引数は与えられた構造ではなく、与えられた動作を提供することが期待されます。
Pythonの関数はこのように、参照可能なあらゆるタイプのデータを受け入れる準備ができており、関数はそれが可能です。
この記事を読んで、Pythonの委譲(delegation)ベースの多態性と参照に飛び込んでみてください。
関数オブジェクト
Pythonはオブジェクト指向のパラダイムを最大限に推し進めているので、常に「すべてはオブジェクトである」という考え方に従うことを重要視しています。そのため、Pythonの関数はオブジェクトであり、次のような簡単な例があります。
code: python
>> def f():
... pass
...
>> type(f)
<class 'function'>
>> type(type(f))
<class 'type'>
>> type(f).__bases__
(<class 'object'>,)
>>
そう考えると、Pythonは関数をファーストクラスオブジェクトのように扱う特別なことはせず、単に他のものと同様にオブジェクトであると認識しています。
呼び出し可能なオブジェクト
Pythonは上の例で見たような明確な関数クラスを持っていますが、それよりも__call__() メソッドの存在に依存しています。つまり、Pythonでは、オブジェクトが「呼ばれる」ときに呼び出されるこのメソッドを持っていれば、どんなオブジェクトでも関数として振る舞うことができるのです。
このことは、デコレーターについての議論で重要になるので、私たちは通常、呼び出し可能なオブジェクトに関心があり、関数にのみ関心があるわけではないことを覚えておいてください。
関数が呼び出し可能であるという事実は、いくつかの簡単なコードで示すことができます。
code: python
>> def f():
... pass
...
>> f.__call__
<method-wrapper '__call__' of function object at 0xb6709fa4>
メタプログラミング
これは言語理論についての投稿ではありませんが、メタプログラミングについては、すこし説明しておく価値があります。通常、「プログラミング」とは、データに変換を加える作業だと考えられています。データと関数は、オブジェクト指向のアプローチによって一緒にすることができますが、それでも2つの異なるものです。しかし、データを変更するためにコードを実行することがあるように、コード自体を変更するためにコードを実行することもあるということにすぐに気がつきます。
低レベルの言語では、これは非常に単純なことです。なぜなら、機械語レベルではすべてがバイトのシーケンスであり、データやコードを変更しても何の違いもないからです。私がx86アセンブリー時代に思い出した最も単純な例は、いくつかのコンピュータウイルスに見られる非常に単純な自己難読化コードです。コードはXOR暗号で暗号化されていて、実行するとまず自分のコードを復号化してから実行するというものでした。このようなトリックの目的は、コードを難読化して、アンチウイルスがウイルスコードを見つけて除去することが困難になるようにすることでした(現在もそうです)。これは非常に原始的なメタプログラミングであり、アセンブリ言語ではコードとデータの間に実質的な区別がないことを認識しているからです。
Pythonのような高レベルの言語では、メタプログラミングを実現するには、バイト値を変更するだけでは不十分です。そのためには、言語自身の構造をデータとして扱う必要があります。言語の一部の動作を変更しようとするとき、私たちは常に実際にメタプログラミングを行っています。通常、最初に思い浮かぶ例はメタクラスです(おそらく名前に「メタ」という言葉が入っているからでしょう)。クラス(言語の一部)は、言語の別の部分(メタクラス)によって作成されます。
デコレーター
メタクラスは、取り扱うには非常にトリッキーで危険なものだと思われがちで、実際にPythonではほとんど必要とされていません。
一方、デコレータは多くの経験豊富なプログラマに愛されている機能であり、その導入後、コミュニティは非常に興味深い適用事例の数々を開発してきました。
デコレーターの関数バージョンは理解するのが少し複雑なので、デコレーターへの最初のアプローチは初心者には難しいと思います。幸いなことに、Pythonではクラスを使ってデコレーターを書くことができるので、全体的に理解しやすく、書きやすいと思います。
そこで、Pythonのデコレーターについて、その理由から始まり、引数のないクラスベースのデコレーター、引数のあるクラスベースのデコレーター、そして最後に関数ベースのデコレーターについて見ていきたいと思います。
理由付け
デコレータとは何でしょうか?また、なぜデコレータの定義と使用方法を学ぶ必要があるのでしょうか?
デコレーターは、関数やクラスの動作を変更する方法で、実際にはメタプログラミングの一種です。デコレーターは、関数やクラスを変更する非常に自然な方法だと思います。
さらに、いくつかの構文上の工夫を加えることで、変更を加え、その変更がなされたことを知らせるための非常にコンパクトな方法となります。
デコレーターの最適な構文は次のようなものです。
code: python
@dec
def func(*args, **kwds):
pass
ここで、dec はデコレータの名前であり、関数 func はこのデコレータによって装飾(decorate)されていると言えます。ご覧のように、誰が見ても、関数に特別なラベルが付けられ、その結果、その動作が変更されていることがすぐにわかります。
しかし、この形式は、より一般的な形式である以下の形式を単純化したものに過ぎません。
code: python
def func(*args, **kwds):
pass
func = dec(func)
では、関数やクラスに変更を加えるとしたら、実際にはどのようなことが考えられるでしょうか。ここでは、属性の追加という非常に単純な作業に絞って考えてみましょう。これは決して無意味な作業ではなく、多くの実用的な使用例があるからです。まず、Pythonで関数に属性を追加する方法を試してみましょう。
code: python
>> def func():
... pass
...
>> func.attr = "a custom function attribute"
>> func.attr
'a custom function attribute'
これをクラスに’すると次のようになります。
code: python
>> class SomeClass:
... pass
...
>> SomeClass.attr = "a custom class attribute"
>> SomeClass.attr
'a custom class attribute'
>> s = SomeClass()
>> s.attr
'a custom class attribute'
ご覧のように、クラスに属性を追加すると正しくクラス属性になり、そのクラスのインスタンスで共有されます(クラス属性と共有についての説明は、こちらの記事をご覧ください)。
引数のないクラスベースのデコレータ
すでに説明したように、Pythonは__call__()メソッドを提供している限り、(関数と同じように)あらゆるオブジェクトを呼び出すことができます。そのため、クラスベースのデコレータを書くには、そのようなメソッドを定義したオブジェクトを作成する必要があります。
デコレーターとして使用する場合、クラスはデコレーション時、つまり関数が定義された時にインスタンス化され、関数が呼ばれた時に呼び出されます。
code: python
class CustomAttr:
def __init__(self, obj):
self.attr = "a custom function attribute"
self.obj = obj
def __call__(self):
self.obj()
ご覧のように、明らかにしなければならないことがすでにたくさんあります。まず最初に、クラスをデコレーターとして使用する場合、デコレーションされるオブジェクトで初期化されます。
デコレーション時に__init__() メソッドが呼び出されますが、デコレータの__call__()メソッドが、デコレーションされるオブジェクトの同じメソッドの代わりに呼び出されます。この場合(引数のないデコレーター)、デコレーターの__call__()メソッドは、引数を受け取りません。この例では、初期化ステップで保存された元の関数に呼び出しを「リダイレクト」しているだけです。
この例では、デコレーションされたオブジェクトの振る舞いを変えることができる2つの異なる瞬間があることがわかります。一つ目は定義時、二つ目は実際に呼び出された時です。
デコレーターは、前のセクションで示した簡単な構文で適用できます。
code: python
@CustomAttr
def func():
pass
Pythonがファイルを解析して関数func()を定義すると、ボンネット内で実行されるコードは次のようになります。
code: python
def func():
pass
func = CustomAttr(func)
をデコレーターの定義にしたがって作成します。このため、クラスは__init__()メソッドのパラメータとして、 デコレーションされたオブジェクトを受け入れなければなりません。
この場合、デコレーション後に得られる func オブジェクトは、関数ではなく CustomAttr オブジェクトになることに注意してください。
code: python
>> func
<__main__.CustomAttr object at 0xb6f5ea8c>
これが、__init__()メソッドでattr属性をobjではなくクラスのインスタンスselfに付けた理由で、これで動作するようになりました。
code: python
>> func.attr
'a custom function attribute'
この置き換えは、__call__()も再定義しなければならない理由でもあります。func()と書いた場合、関数を実行しているのではなく、デコレーションが返すCustomAttrのインスタンスを呼び出しているのです。
引数を持つクラスベースのデコレータ
このケースは、これまでのステップを超えた最も自然なステップです。メタプログラミングを始めたら、それをスタイリッシュにやりたいと思うでしょう。デコレータにパラメータを追加することは、メタプログラミングのコードを一般化するという唯一の目的があります。ちょうど、値をハードコーディングする代わりにパラメータ化された関数を書くときのように。
ここで、大きな注意点があります。クラスベースのデコレータで引数を持つものは、引数を持たないものとは若干異なる動作をします。具体的には、__call__()メソッドはデコレーションの最中に実行され、呼び出しの最中には実行されません。
まず、構文を確認しましょう。
code: python
class CustomAttrArg:
def __init__(self, value):
self.value = value
def __call__(self, obj):
obj.attr = "a custom function attribute with value {}".format(self.value)
return obj
@CustomAttrArg(1)
def func():
pass
ここで、__init__()メソッドは、Python関数の標準的なルールである名前付き引数とデフォルトの引数を使って、いくつかの引数を受け取ります。__call__()メソッドは装飾されたオブジェクトを受け取りますが、前のケースでは__init__()メソッドに渡されていました。
しかし、最大の変更点は、__call__() が、デコレーションされたオブジェクトを呼び出したときに実行されるのではなく、デコレーションの段階である __init__() の直後に実行されることです。これにより、以前はデコレーションされたオブジェクトはそれ自体ではなくデコレーターのインスタンスでしたが、 今はデコレーションされたオブジェクトが __call__() メソッドの戻り値になります。
デコレーションされたオブジェクトを呼び出すときには、__call__() から得られるものを実際に呼び出すことになるので、必ず意味のあるものを返すことを覚えておいてください。
上の例では、__init__() に1つの引数を格納し、この引数は関数に適用するときにデコレータに渡されます。そして、__call__() では、格納された引数を使ってデコレーションされたオブジェクトの属性を設定し、オブジェクト自体を返します。ここで重要なのは、呼び出し可能なオブジェクトを返さなければならないということです。
つまり、デコレーションされたオブジェクトで何か複雑なことをしなければならない場合、そのオブジェクトを利用するローカル関数を定義し、その関数を返せばよいのです。非常に簡単な例を見てみましょう。
code: python
class CustomAttrArg:
def __init__(self, value):
self.value = value
def __call__(self, obj):
def wrap():
# Here you can do complex stuff
obj()
# Here you can do complex stuff
return wrap
@CustomAttrArg(1)
def func():
pass
ここでは、返されるオブジェクトは装飾されたものではなく、ローカルに定義された新しい関数 wrap() となります。Pythonがそれをどのように識別するかを示すのは興味深いことです。
code: python
>> @CustomAttrArg(1)
... def func():
... pass
...
>> func
<function CustomAttrArg.__call__.<locals>.wrap at 0xb70185cc>
このパターンでは、デコレーションされたオブジェクトを使ってさまざまなことが可能になります。デコレーションされたオブジェクトを変更する(属性を追加するなど)だけでなく、その結果を事前または事後にフィルタリングすることもできます。私たちがメタプログラミングと呼んでいるものは、よくわからない魔法のようなものではなく、日常的な作業にとても役立つものだということがわかってきたのではないでしょうか。
デコレーターとプロトタイプ
クラスベースのデコレータを引数付きで書いた場合、あなたは任意の呼び出し可能なオブジェクトを返す役割を担っています。返されたオブジェクトがデコレーションされたオブジェクトと同じプロトタイプを持つことが通常のケースであっても、返されたオブジェクトには何の前提もありません。
つまり、デコレートされたオブジェクトがゼロ個の引数を受け入れる場合(この例のように)、通常はゼロ個の引数を受け入れるcallableを返すことになります。しかし、これは決して言語によって強制されているわけではなく、これによってメタプログラミングのテクニックを少し押し上げることができます。ただし、この記事ではこのテクニックの例は示しません。
関数ベースのデコレータ
関数ベースのデコレータは、単純なケースでは非常にシンプルですが、 複雑なケースでは少し厄介です。問題は、デコレーターを見たことがない人にとっては、その構文を一見して理解するのが難しいことです。これは、クラスベースのデコレータで引数がある場合とあまり変わらず、 デコレートされたオブジェクトをラップして返すローカル関数を定義します。
引数なしの場合は、常に最も単純なものです。
code: python
def decorate(f):
def wrap():
f()
return wrap
@decorate
def func():
pass
これは、クラスと同等のケースと同様の動作です。この関数は、デコレーションされたオブジェクトを渡して呼び出すデコレーション処理によって、decorate() 関数の引数として渡されます。しかし、実際にこの関数を呼び出すときには、実際に wrap() を呼び出していることになります。
クラスベースのデコレータの場合と同様に、パラメータ化することで呼び出しの手順が変わります。これは、次のコードのようになります。
code: python
def decorate(arg1):
def wrap(f):
def _wrap(arg):
f(arg + arg1)
return _wrap
return wrap
@decorate(1)
def func(arg):
pass
ご覧のように、これは本当に簡単なことではありません。これが、私が最後のケースとしてこの話をしたかった理由です。クラスベースのデコレータについて学んだことを思い出してください。デコレータが引数で呼ばれたときに、最初に呼び出されるのが decorate() 関数です。したがって @decorate(1) は arg1 に 1 を渡して decorate() を呼び出し、この関数は wrap() というローカル関数を返します。
この2つ目の関数は、別の関数を引数として受け取り、実際のデコレーション処理で使用されています。このwrap() 関数は、func() をデコレートするために使われていますが、互換性のあるオブジェクト、つまりこの場合は1つの引数を受け取る関数を返したいと考えています。そのため、wrap() は、func() に渡された引数とデコレータに渡された引数の両方を最終的に使用する _wrap() ローカル関数を定義して返すのです。
つまり、このプロセスは次のように要約されます(何が起こっているかを示すために、デコレーションされた関数を不適切にfunc_decと呼ぶことにします)。
decorator(1)は(引数を知っている)wrap()関数を返す
func() は func_dec = wrap(func) becoming _wrap() と再定義されます。
func_dec(arg) を呼び出すと、Python は _wrap(arg) を実行し、1 + arg を引数として元の func() を呼び出します。明らかにデコレーターの概念の力は、func_dec() ではなく func() 自体を扱うことであり、すべての "魔法 "はシステムの裏側で起こるのです。
関数ベースのデコレーターに違和感を覚える方も心配ありません。私は通常、単純なケース(たとえばクラスの属性を設定する)では関数ベースのデコレータを使い、コードが複雑になってきたらクラスベースのものに移行しています。
使用例
デコレータの威力がよくわかる例として、functools.total_ordering があります。functoolsモジュールはPythonの関数型アプローチを推し進めるために多くの興味深いツールを提供していますが、中でもpartial() と partialmethod() が有名です。
total_ordering デコレーターは、オブジェクトが比較順序付けメソッドの小さなセットから完全なセットを提供するようにします。比較メソッドとは、Pythonが2つのオブジェクトを比較するときに呼び出すメソッドのことです。例えば、a == b と書くと、Python は a.__eq__(b) を実行し、他の5つの演算子 > (__gt__)、< (__lt__)、>= (__ge__)、 <= (__le__)、 != (__ne__) についても同じことが起こります。 数学的には、これらすべての演算子は、それらのうちの一つと __eq__() メソッドだけを使って表現することができます。このデコレータは、この事実を利用して、装飾されたオブジェクトに不足しているメソッドを提供します。
code: python
class Person:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def __lt__(self, other):
return self.name < other.name
これは、比較メソッド == と< を定義するシンプルなクラスです。
code: python
>> p1 = Person('Bob')
>> p2 = Person('Alice')
>> p1 == p2
False
>> p1 < p2
False
>> p1 >= p2
Traceback (most recent call last):
File "/home/leo/prova.py", line 103, in <module>
p1 >= p2
TypeError: unorderable types: Person() >= Person()
大きな警告です。Python は > と != の比較を実行しようとしても文句を言いませんが、専用のメソッドがない場合は「標準」の比較を実行します。これは、ドキュメントに書かれている ように、「比較演算子の間には暗黙の関係はない」ということです。つまり、x == y が真であることは、x != y が偽であることを暗示しません。 しかし、total_orderingデコレーターを使うと、6つの比較がすべて可能になります。
code: python
import functools
@functools.total_ordering
class Person:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
def __lt__(self, other):
return self.name < other.name
code: python
>> p1 = Person('Bob')
>> p2 = Person('Alice')
>> p1 == p2
False
>> p1 != p2
True
>> p1 > p2
True
>> p1 < p2
False
>> p1 >= p2
True
>> p1 <= p2
False
最後に
デコレータは非常に強力なツールであり、学ぶ価値があります。メタプログラミングの素晴らしい世界への第一歩になるかもしれませんし、コードを単純化するための高度なテクニックになるかもしれません。どのような使い方をするにしても、2つのケース(引数がある場合と、ない場合)の違いを理解し、構文が少し複雑だからといって関数ベースのデコレータを避けることのないようにしましょう。
参考
Python 3 Patterns, Recipes and Idioms のページの元になった(と思われる)Bruce Eckel の3つの投稿 に感謝します(後者はまだ進行中です)。Update: Bruce が述べたように、「Python3 Patterns book は失敗したプロジェクトのようなもの」です。ですから、そこに含まれる情報は更新されないことに注意してください。しかし、この本はPythonの研究や調査のための良い出発点です。 Graham Dumpleton は Python デコレーターの非常に興味深い詳細な分析をここに書いています。